/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2014-2019.
 * Description: idump elf bin format
 * Author: nixiaoming
 * Create: 2014-6-18
 */

#include <linux/binfmts.h>
#include <linux/slab.h>
#include <linux/pagemap.h>
#include <linux/elf.h>
#ifdef CONFIG_WRNOTE
#include <linux/wrnote.h>
#endif
#include <linux/vmalloc.h>
#include <linux/mm.h>
#include <linux/idump.h>
#include <linux/types.h>
#include "binfmt_idump.h"

/* check section name string table header */
static int check_elf_shstrtab(const struct elf_shdr *shdr)
{
	if (shdr->sh_type != SHT_STRTAB) {
		pr_info("IDUMP: elf sh_strtab type error.\n");
		return -EINVAL;
	}

	if (shdr->sh_size == 0 || shdr->sh_offset < sizeof(struct elfhdr)) {
		pr_info("IDUMP: elf sh_strtab format error.\n");
		return -EINVAL;
	}

	return 0;
}

static ssize_t get_elf_shstrtab(const struct elfhdr *elf_hdr, struct file *elffile,
				struct elf_shdr *shdr)
{
	ssize_t retval;
	loff_t shdr_offset;
	size_t size = sizeof(struct elf_shdr);

	/* Calculate offset of section header string table */
	shdr_offset = elf_hdr->e_shoff + (elf_hdr->e_shstrndx * size);

	/* First read the section header of sh_string table */
	retval = kernel_read(elffile, shdr, size, &shdr_offset);
	if (retval != size) {
		pr_info("IDUMP: read string section hdr error:%d\n", (int)retval);
		return retval < 0 ? retval : -EIO;
	}

	retval = (ssize_t)check_elf_shstrtab(shdr);
	return retval;
}

static ssize_t get_sh_string(struct file *elffile, const struct elf_shdr *shdr,
			     char *sh_str_tab, const size_t sh_str_size)
{
	ssize_t retval;
	loff_t pos = (loff_t)shdr->sh_offset;

	/* Copy the sh_string table for reference */
	retval = kernel_read(elffile, sh_str_tab, sh_str_size, &pos);
	if (retval != sh_str_size) {
		printk(KERN_INFO "IDUMP: read section string table error:%d\n", (int)retval);
		return retval < 0 ? retval : -EIO;
	}

	return 0;
}

static ssize_t load_got_dyn(const struct elfhdr *elf_hdr, struct file *elffile,
			      struct got_dyn *info, const char *sh_str_tab,
			      const size_t sh_str_size)
{
	int i;
	loff_t pos;
	ssize_t retval;
	struct elf_shdr shdr;
	loff_t shdr_offset = elf_hdr->e_shoff;

	/* loop through section headers to identify .got and .dynamic sections */
	for (i = 0; i < elf_hdr->e_shnum; i++) {
		/* read the section header table */
		pos = (loff_t)(shdr_offset + (i * sizeof(shdr)));
		retval = kernel_read(elffile, &shdr, sizeof(shdr), &pos);
		if (retval != sizeof(shdr)) {
			printk(KERN_INFO "IDUMP: read section hdr table error:%d\n", (int)retval);
			return retval < 0 ? retval : -EIO;
		}

		/* secton name index must lower than sction name string table size */
		if (shdr.sh_name >= sh_str_size) {
			printk(KERN_INFO "IDUMP: Elf setcion %d name index %d > sh_str_size\n",
			       i, shdr.sh_name);
			/*
			 * though there is an error, but it may not affect elf exeution.
			 * so, continue and try our best to do idump .
			 */
			continue;
		}

		/* Find the section header name from sh_string_tabe */
		if ((shdr.sh_type == SHT_DYNAMIC) &&
		    !strcmp(".dynamic", &sh_str_tab[shdr.sh_name]))
			memcpy(&info->dynamic, &shdr, sizeof(shdr));
		else if ((shdr.sh_type == SHT_PROGBITS) &&
			 !strcmp(".got", &sh_str_tab[shdr.sh_name]))
			memcpy(&info->got, &shdr, sizeof(shdr));
		else
			continue;
	}

	return 0;
}

static int idump_load_got_dyn(const struct elfhdr *elf_hdr, struct file *elffile,
		 struct got_dyn *info)
{
	ssize_t retval;
	struct elf_shdr shstrtab;
	size_t sh_str_size;
	char *sh_str_tab = NULL;

	retval = get_elf_shstrtab(elf_hdr, elffile, &shstrtab);
	if (retval < 0)
		return retval;

	sh_str_size = shstrtab.sh_size;
	sh_str_tab = vmalloc(sh_str_size);
	if (sh_str_tab == NULL) {
		pr_info("IDUMP: failed to vmalloc for sh_strtab.\n");
		return -ENOMEM;
	}

	retval = get_sh_string(elffile, &shstrtab, sh_str_tab, sh_str_size);
	if (retval < 0)
		goto out;

	retval = load_got_dyn(elf_hdr, elffile, info, sh_str_tab, sh_str_size);
out:
	vfree(sh_str_tab);
	return retval;
}

static bool section_in_segment(struct elf_shdr *sh, struct elf_phdr *ph)
{
	if ((sh->sh_offset >= ph->p_offset)
		&& (sh->sh_offset - ph->p_offset <= ph->p_filesz - 1)
		&& (sh->sh_offset - ph->p_offset + sh->sh_size <= ph->p_filesz)
		&& (sh->sh_addr >= ph->p_vaddr)
		&& (sh->sh_addr - ph->p_vaddr <= ph->p_memsz - 1)
		&& (sh->sh_addr - ph->p_vaddr +  sh->sh_size <= ph->p_memsz)
		&& (sh->sh_size != 0)
		&& (ph->p_memsz != 0))
		return true;
	return false;
}

static inline unsigned long count_section_vaddr(const struct elf_shdr *sh, struct elf_phdr *ph,
		unsigned long base)
{
	return base + (sh->sh_addr - idump_align_down(ph->p_vaddr, PAGE_SIZE));
}

static void idump_count_got_dyn_vaddr(struct got_dyn *info, struct elf_phdr *elf_ppnt,
		unsigned long base)
{
	struct rtos_mm_struct *rtos_mm = mm_to_rtos_mm(current->mm);

	if (section_in_segment(&info->got, elf_ppnt)) {
		rtos_mm->idump.elf_info.got_vaddr = count_section_vaddr(&info->got, elf_ppnt, base);
		rtos_mm->idump.elf_info.got_size = (unsigned long)info->got.sh_size;
	}
	if (section_in_segment(&info->dynamic, elf_ppnt)) {
		rtos_mm->idump.elf_info.dyn_vaddr = count_section_vaddr(&info->dynamic, elf_ppnt, base);
		rtos_mm->idump.elf_info.dyn_size = (unsigned long)info->dynamic.sh_size;
	}
}
